using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using System.Diagnostics;
using Microsoft.Xna.Framework.Input;

namespace Dragoon.BreakIt
{

    public class Wall : Bounded2D
    {
        public readonly Side Side;
        internal Wall(Rectangle playArea, Side side)
        {
            const int THICKNESS = 50;
            Point leftTop, rightBottom;
            Side = side;
            switch (Side)
            {
                case Side.Left:
                    leftTop =     new Point(playArea.Left - THICKNESS, playArea.Top - THICKNESS);
                    rightBottom = new Point(playArea.Left, playArea.Bottom + THICKNESS);
                    break;
                case Side.Right:
                    leftTop = new Point(playArea.Right, playArea.Top - THICKNESS);
                    rightBottom = new Point(playArea.Right + THICKNESS, playArea.Bottom + THICKNESS);
                    break;
                case Side.Top:
                    leftTop =     new Point(playArea.Left - THICKNESS, playArea.Top - THICKNESS);
                    rightBottom = new Point(playArea.Right + THICKNESS, playArea.Top);
                    break;
                case Side.Bottom:
                    leftTop =     new Point(playArea.Left - THICKNESS, playArea.Bottom);
                    rightBottom = new Point(playArea.Right + THICKNESS, playArea.Bottom + THICKNESS);
                    break;
                default:
                    throw new ExDragoon("Invalid side value");
            }
            _boundingBox = new BoundingBox(MakeVector3(leftTop,-1), MakeVector3(rightBottom,1));
        }

        public override string ToString()
        {
            return string.Format("{0} wall ({1})", Side, _boundingBox);
        }
    }


    public enum GameState
    {
        NotStarted, // the first game has not started
        Launching,  // the ball is on the launch pad
        Bouncing,   // the ball is moving
        Crashing,   // the ball is crashing
        GameOver,   // the game is over 
        QuitProgram // the user decied to quit the program
    }

    enum TransisionCue
    {
        None,
        FloorHit,       // the ball hit the floor
        LevelComplete,  // the level is complete
        Quit            // the user wants to quit  
    }

    public class GameEngine
    {
        const float INITIAL_BALL_SPEED = 400; // pixels per second
        const float SPEED_INCREMENT = 0.05F;   // "percent" per level
        const int INITIAL_LIVES = 3;
        /// <summary>
        /// Bonus allocated when a level starts
        /// </summary>
        const int INITIAL_BONUS = 999;
        private readonly Configuration Config;
        /// <summary>
        /// The high score of this "session"
        /// </summary>
        private int _highScore = 0;
        /// <summary>
        /// The max Y position of the ball prevents collision anomalies on
        /// the paddle
        /// </summary>
        private readonly int _ballMaxY; 
        private Ball _ball;
        private Wall _leftWall, _roof, _rightWall, _floor;
        private CollisionSet _allCollidables = new CollisionSet();
        private CollisionSet _fixedColidables = new CollisionSet();
        private CollisionSet _capsuleObsticles = new CollisionSet();
        private List<Capsule> _activeCapsules = new List<Capsule>();
        /// <summary>
        /// Zero based number indicating at which level the user is.
        /// </summary>
        private int _activeLevelNumber;
        private int _score = 0;
        private double _levelStartTime;
        private readonly Paddle _paddle;
        private int _lives;
        private GameState _state;
        private Menu _startMenu, _newGameMenu, _activeMenu;
        private int _bonus;
        /// <summary>
        /// 
        /// </summary>
        /// <param name="config"></param>
        /// <param name="playSurface">the "inner" surface of the play area in pixels</param>
        /// <param name="paddleHeight">the height of the paddle in pixels</param>
        /// <param name="ballRadius">the radius of the ball in pixels</param>
        public GameEngine(Configuration config,             
            int ballRadius)
        {
            Config = config;
            _lives = INITIAL_LIVES;

            // Create the walls
            _leftWall = new Wall(Config.PlayArea, Side.Left);
            _roof = new Wall(Config.PlayArea, Side.Top);
            _rightWall = new Wall(Config.PlayArea, Side.Right);
            _floor = new Wall(Config.PlayArea, Side.Bottom);

            _allCollidables.Items.Add(_roof);
            _allCollidables.Items.Add(_rightWall);
            _allCollidables.Items.Add(_floor);
            _allCollidables.Items.Add(_leftWall);

            _capsuleObsticles.Items.Add(_floor);
            int paddleHeight = ballRadius * 3;
            _ballMaxY = Config.PlayArea.Bottom - paddleHeight - ballRadius;
            _ball = new Ball(new Vector3(
                Config.PlayArea.Left + (Config.PlayArea.Width / 2),
                Config.PlayArea.Bottom - ballRadius * 6,
                0),
                ballRadius);

            // all collidables except _paddle is not fixed
            _fixedColidables.Items.AddRange(_allCollidables.Items); 

            _paddle = new Paddle(Config.PlayArea, paddleHeight);
            _allCollidables.Items.Add(_paddle);
            _capsuleObsticles.Items.Add(_paddle);
            _state = GameState.NotStarted;

            _startMenu = new Menu(new string[] {
                "",
                "___________________________________",
                "",
                "Space to launch", 
                "Left and Right to move", 
                "",
                "A,B,C - new size ", 
                "D - new life",
                "___________________________________",
                ""
                }, 
                new string[] {
                    "Start game", 
                    "Quit"});
            _newGameMenu = new Menu(new string[] { 
                "",
                "___________________________________",
                "",
                "Game over",
                "YOUR SCORE",
                "BEST HIGH",
                "",
                "___________________________________",
                ""
 },
                new string[] {
                    "Play again", 
                    "Quit"});
            _startMenu.OnClose += new EventHandler<MenuEvent>(menuClose);
            _newGameMenu.OnClose += new EventHandler<MenuEvent>(menuClose);
            _activeMenu = _startMenu;
            SetLevel(0);
        }

        public GameEngine(Configuration config)
            : this(config,
                config.PlayArea.Height / 80)
        { }

        void menuClose(object sender, MenuEvent e)
        {
            NextState(e.Time, e.HasSelected? TransisionCue.None : TransisionCue.Quit);
                
        }

        public Menu ActiveMenu
        {
            get { return _activeMenu; }
        }

        public int Score
        {
            get { return _score; }
        }

        public IEnumerable<ICollidable> Collidables
        {
            get { return _allCollidables.Items; }
        }

        public Rectangle InnerPlayArea
        {
            get { return Config.PlayArea; }
        }

        private void SendBall(Vector3 startposition, Vector3 direction)
        {
            if (startposition.Y > _ballMaxY)
                startposition.Y = _ballMaxY;
            Ray heading = new Ray(startposition, direction);
            ICollidable hitObj;
            float distance;
            if (!_fixedColidables.Intersects(heading, out hitObj, out distance))
                throw new ExDragoon("Ball went out of bounds. Direction is {0}",direction);
            _ball.SetCourse(heading, distance-Ball.Radius);
        }

        public Level ActiveLevel
        {
            get { return Config.Levels[_activeLevelNumber%Config.Levels.Count]; }
        }

        private void StartLevel(GameTime time)
        {
            _levelStartTime = time.TotalGameTime.TotalSeconds;
        }


        public void NextState(GameTime time)
        {
            NextState(time, TransisionCue.None);
        }

        private void NextState(GameTime time,TransisionCue cue)
        {
            if (cue == TransisionCue.Quit)
            {
                _state = GameState.QuitProgram;
                return;
            }

            switch (_state)
            {
                case GameState.NotStarted:
                    StartLevel(time);
                    _state = GameState.Launching;
                    _activeMenu = null;
                    break;
                case GameState.Launching:
                    SendBall(_ball.Origin, Geometry.Down);
                    _paddle.ResetLimits();
                    _state = GameState.Bouncing;
                    break;
                case GameState.Bouncing:
                    if (cue == TransisionCue.FloorHit)
                    {
                        _lives--;
                        if (Lives == 0)
                        {
                            _state = GameState.GameOver;
                            if (_score > _highScore)
                            {
                                _newGameMenu.ItemAt(4).Text = string.Format("{0} is a new record", _score);
                                _newGameMenu.ItemAt(5).Text = "Congratulations!";
                                _highScore = _score;
                            }
                            else
                            {
                                _newGameMenu.ItemAt(4).Text = string.Format("You scored {0}!", _score);
                                _newGameMenu.ItemAt(5).Text = string.Format("The current record is {0}", _highScore);
                            }
                            _activeMenu = _newGameMenu;
                        }
                        else
                        {
                            _ball.Initialise();
                            _paddle.Intialise();
                            _state = GameState.Launching;
                        }
                    }
                    else if (cue == TransisionCue.LevelComplete)
                    {
                        _score += Bonus;
                        if (OnLevelComplete != null)
                            OnLevelComplete(this, new VoidEvent());
                        SetLevel(_activeLevelNumber + 1);
                        StartLevel(time);
                        _state = GameState.Launching;
                    }
                    break;
                case GameState.GameOver:
                    _score = 0;
                    _lives = INITIAL_LIVES;
                    SetLevel(0);
                    StartLevel(time);
                    _state = GameState.Launching;
                    _activeMenu = null;
                    break;
            }
        }

        public GameState State
        {
            get { return _state; }
        }

        /// <summary>
        /// The level number of the user. Starts with 1.
        /// </summary>
        /// <remarks>
        /// For a game, this number always increments, even when the user reached 
        /// the final level, and starts again with the first one.
        /// </remarks>
        public int LevelNumber
        {
            get { return _activeLevelNumber + 1; }
        }

        public int Bonus
        {
            get { return _bonus; }
        }

        private void SetLevel(int levelNumber)
        {
            // remove all falling capsules
            _activeCapsules.Clear();

            // remove all remaining blocks from collidables
            foreach (ICollidable co in ActiveLevel.Blocks)
                _allCollidables.Items.Remove(co);

            _activeLevelNumber = levelNumber;
            // add all blocks to collision engine
            foreach (Block b in ActiveLevel.Blocks)
            {
                b.Initialise();
                Debug.Assert(b.IsActive);
                _allCollidables.Items.Add(b);
            }
            _ball.Initialise();
            _paddle.Intialise();
            _ball.Speed = INITIAL_BALL_SPEED + SPEED_INCREMENT * (levelNumber-1);
        }

        public event EventHandler<CollisionEvent> OnCollision;
        public event EventHandler<VoidEvent> OnLevelComplete;

        /// <summary>
        /// The _lastCleanLocation is needed to provent the ball
        /// from sinking into the object it collides with
        /// </summary>
        private Vector3 _lastCleanLocation;

        /// <summary>
        /// Check if the ball hits anything and bounce if it does
        /// </summary>
        /// <param name="time"></param>
        private void UpdateBall(GameTime time)
        {
            Vector3 ballPos = _ball.LocationAt(time);
            BoundingSphere sphere = new BoundingSphere(ballPos, _ball.Radius);
            ICollidable objectHit;
            Side sideHit;
            if (_allCollidables.Intersects(sphere, out objectHit, out sideHit))
            {
                // Handle the collision
                if (objectHit == _floor)
                {
                    NextState(time,TransisionCue.FloorHit);
                }
                else
                {
                    if (objectHit is Block)
                    {
                        Block blockHit = objectHit as Block;
                        blockHit.Hit();
                        if (!blockHit.IsActive)
                        {
                            if (blockHit.HasCapsule)
                            {
                                _activeCapsules.Add(blockHit.HiddenCapsule);
                                blockHit.HiddenCapsule.StartFall(time);
                            }
                            _allCollidables.Items.Remove(blockHit);
                            if (!ActiveLevel.HasActiveBlocks)
                            {
                                NextState(time, TransisionCue.LevelComplete);
                                return;
                            }
                        }
                        _score += blockHit.Type.Reward;
                    }
                    // bounce the ball
                    Vector3 newDirection = _ball.Velocity;
                    Point boxCenter =
                        new Point(
                        (int)((objectHit.Bounds.Max.X - objectHit.Bounds.Min.X) / 2),
                        (int)((objectHit.Bounds.Max.Y - objectHit.Bounds.Min.Y) / 2));
                    if (objectHit == _paddle)
                    {
                        if (sideHit == Side.Bottom)
                            NextState(time, TransisionCue.FloorHit);
                        Vector3 paddlePos = Geometry.ConvertCenterToVector(_paddle.Position);
                        newDirection.Y = -_paddle.Position.Width/2;
                        if (boxCenter.X < paddlePos.X) // left of center
                            newDirection.X = -(paddlePos.X - ballPos.X);
                        else
                            newDirection.X = (ballPos.X - paddlePos.X);
                        newDirection.Normalize();
                    }
                    else
                    {
                        switch (sideHit)
                        {
                            case Side.Left:
                            case Side.Right:
                                newDirection.X *= -1.0F;
                                break;
                            case Side.Top:
                            case Side.Bottom:
                                newDirection.Y *= -1.0F;
                                break;
                            case Side.CnrLeftTop:
                                newDirection.X = (boxCenter.X - ballPos.X);
                                newDirection.Y = (boxCenter.Y - ballPos.Y);
                                newDirection.Normalize();
                                break;
                            case Side.CnrLeftBottom:
                                newDirection.X = (boxCenter.X - ballPos.X);
                                newDirection.Y = -(boxCenter.Y - ballPos.Y);
                                newDirection.Normalize();
                                break;
                            case Side.CnrRightBottom:
                                newDirection.X = -(boxCenter.X - ballPos.X);
                                newDirection.Y = -(boxCenter.Y - ballPos.Y);
                                newDirection.Normalize();
                                break;
                            case Side.CnrRightTop:
                                newDirection.X = -(boxCenter.X - ballPos.X);
                                newDirection.Y = (boxCenter.Y - ballPos.Y);
                                newDirection.Normalize();
                                break;
                        };
                    }
                    SendBall(_lastCleanLocation, newDirection);
                }
                GC.Collect(); GC.WaitForPendingFinalizers();
                // fire a collision event
                if (OnCollision != null)
                    OnCollision(this, new CollisionEvent(ballPos, _ball, objectHit));
            }
            else
                _lastCleanLocation = ballPos;
        }
     
        private void UpdateCapsules(GameTime time)
        {
            foreach (Capsule c in _activeCapsules)
                c.Update(time);
            Capsule capsuleHit;
            ICollidable obstacleHit;
            if (_capsuleObsticles.FindFirstHit(_activeCapsules, out capsuleHit, out obstacleHit))
            {
                capsuleHit.Stop(time);
                _activeCapsules.Remove(capsuleHit);
                if (obstacleHit == _paddle)
                {
                    switch (capsuleHit.Type.Type)
                    {
                        case ModType.ChangePaddleLength:
                            if (capsuleHit.Type.ValueAsDouble > 0)
                                _paddle.Size = PaddleSize.Long;
                            else if (capsuleHit.Type.ValueAsDouble < 0)
                                _paddle.Size = PaddleSize.Short;
                            else
                                _paddle.Size = PaddleSize.Normal;
                            break;
                        case ModType.ExtraBall:
                            _lives++;
                            break;
                    }
                    // fire a collision event
                    if (OnCollision != null)
                        OnCollision(this, new CollisionEvent(
                            Geometry.ConvertCenterToVector(capsuleHit.Position), capsuleHit, _paddle));
                }

            }
        }
        
        public void Update(GameTime time)
        {
            if (_state == GameState.Bouncing)
                UpdateBall(time);
            if (_state == GameState.Bouncing || _state == GameState.Crashing)
            {
                // update bonus
                _bonus = INITIAL_BONUS - (int)(time.TotalGameTime.TotalSeconds - _levelStartTime);
                if (_bonus < 0)
                    _bonus = 0;
            }

            UpdateCapsules(time);
            _paddle.Update(time);
        }

        public Ball Ball
        {
            get { return _ball; }
        }
        
        public Paddle Paddle
        {
            get {return _paddle;}
        }

        public void HandleInput(GameTime time)
        {
            if (_state == GameState.GameOver)
                return;
            bool lkey = Keyboard.GetState().IsKeyDown(Keys.Left);
            bool rkey = Keyboard.GetState().IsKeyDown(Keys.Right);
            if (lkey && rkey || (!lkey && !rkey))
            {
                if (_paddle.IsMoving) _paddle.Stop(time);
            }
            else if (lkey)
            {
                if (!_paddle.IsMoving) _paddle.MoveLeft(time);
            }
            else if (rkey)
            {
                if (!_paddle.IsMoving) _paddle.MoveRight(time);
            }
            if (_state == GameState.Launching)
            {
                if (Keyboard.GetState().IsKeyDown(Keys.Space))
                    NextState(time);
            }
        }

        public int Lives
        {
            get
            {
                return _lives;
            }
        }
    }
}
